게시판 만들기

✒️ 2025-05-26 11:07 내용 수정


실습 목표

실습 흐름

  1. 데이터베이스에 테이블 및 시퀀스, 필요 시 샘플 데이터 추가
  2. 데이터베이스에 연결 : context.xml 파일, 라이브러리(JDBC)
  3. 테이블의 정보를 저장할 DTO 클래스 생성
  4. 데이터베이스에서 조회, 추가, 삭제를 수행하는 DAO 클래스 생성
  5. 게시글과 댓글을 등록, 삭제, 조회할 JSP를 생성
  6. 페이징 처리를 담당할 클래스 생성
  7. DAO 클래스 객체를 생성하고, JSP와 연결할 Servlet 생성
  8. Servlet에서 코드 실행 시 결과 확인

DB에 테이블 추가

--시퀀스
CREATE SEQUENCE SEQ_BOARD_IDX;

--테이블
CREATE TABLE BOARD(
	IDX NUMBER(3) PRIMARY KEY,  --번호
	NAME VARCHAR2(100) NOT NULL, --작성자
	SUBJECT VARCHAR2(255) NOT NULL, --게시글 이름
	CONTENT CLOB,  --게시글 내용
	PWD VARCHAR2(100),  --비밀번호
	IP VARCHAR2(100),  --IP
	REGDATE DATE,  --작성일
	READHIT NUMBER(3) DEFAULT 0, --조회수
	REF INT, --기준글번호(댓글의 메인글 번호)
	STEP INT,  --댓글순서
	DEPTH INT,  --대댓글
	DEL_INFO NUMBER(2)  --글 삭제여부
);

-- 샘플 데이터 추가
INSERT INTO BOARD VALUES(
	SEQ_BOARD_IDX.nextVal,
	'에이스',
	'게시판 첫 글',
	'게시판 첫 번째 글은 내가 작성했다',
	'1234',
	'192.0.0.2',
	SYSDATE,
	0,
	SEQ_BOARD_IDX.currval,
	0,
	0,
	0
);

-- 댓글 샘플 데이터
INSERT INTO BOARD VALUES(
	SEQ_BOARD_IDX.nextVal,
	'브라보',
	'오늘 날씨',
	'오늘 날씨 비 오고 눈 내림',
	'1234',
	'192.0.0.2',
	SYSDATE,
	0,
	1,
	1,
	1,
	0
);

-- 댓글의 댓글 샘플 데이터
INSERT INTO BOARD VALUES(
	SEQ_BOARD_IDX.nextVal,
	'칼리',
	'대댓글',
	'대대대대댓글',
	'1234',
	'192.0.0.3',
	SYSDATE,
	0,
	1,
	2,
	2,
	0
);

게시판 구조.png

DB 연결

  1. context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
	<Resource 
	        auth="Container" 
      		name="jdbc/oracle_test"
      		type="javax.sql.DataSource"
      		driverClassName="oracle.jdbc.driver.OracleDriver"
      		factory="org.apache.commons.dbcp.BasicDataSourceFactory"
      		url="jdbc:oracle:thin:@localhost:1521:xe"
      		username="계정명" password="비밀번호" 
      		maxActive="20" maxIdle="10" maxWait="1"/>
</Context>
  1. 라이브러리
    • 라이브러리에 lomboc 추가하여 사용
파일
commons-collections-3.2.1.jar
commons-dbcp-1.2.2.jar
commons-pool-1.4.jar
ojdbc8-23.3.0.23.09.jar
lomboc.jar
mybatis-3.1.1.jar
  1. MybatisConnector 클래스
package service;

import java.io.Reader;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class MybatisConnector {
	static MybatisConnector single = null;
	
	SqlSessionFactory factory = null;
	
	public static MybatisConnector getInstance() {
		if (single == null) {
			single = new MybatisConnector();
		}
		return single;
	}
	
	public MybatisConnector() {
		try {
			// sqlMapConfig.xml 파일 읽어오기
			Reader reader = Resources.getResourceAsReader("config/mybatis/sqlMapConfig.xml");
			// SqlSessionFactoryBuilder로 SqlSessionFactory 만들기
			factory = new SqlSessionFactoryBuilder().build(reader);
			
			reader.close();
		} catch (Exception e) {
		}
	}
	
	// sqlMapConfig.xml의 정보를 담고 있는 factory 객체 반환
	public SqlSessionFactory getsqlSessionFactory() {
		return factory;
	}
}
  1. SqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

	<!--  별칭을 주면 mapper에서 별칭과 대응되는 패키지와 클래스를 인식 -->
	<typeAliases>
		<typeAlias type="dto.BoardDTO" alias="board"/>
	</typeAliases>

	<environments default="">
		<environment id="">
			<transactionManager type="JDBC" />
			<dataSource type="JNDI">
				<property name="data_source" 
				value="java:comp/env/jdbc/oracle_test" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="config/mybatis/mapper/board.xml" />
	</mappers>
</configuration>

Ajax

  1. HttpRequest.js
var xhr = null;

function createRequest() {
	if (xhr != null) {
		return;
	}
	if (window.ActiveXObject) {
		xhr = new ActiveXObject("Microsoft.XMLHTTP"); // IE 환경
	} else {
		xhr = new XMLHttpRequest(); // 기타 브라우저 환경
	}
}

function sendRequest(url, param, callback, method) {

	// HttpRequest 생성
	createRequest();

	// 전송 타입 구분
	var httpMethod = (method != 'POST' && method != 'post') ? 'GET' : 'POST';

	// 파라미터 구분
	var httpParam = (param == null || param == '') ? null : param;

	// 접근 url
	var httpURL = url;

	// 요청 방식이 GET이고 전달할 파라미터가 있다면 새 url 경로 제작
	if (httpMethod == 'GET' && httpParam != null) {
		httpURL = httpURL+'?'+httParam;
	}

	// 서버로 보낼 Ajax 요청 형식
	xhr.open(httpMethod, httpURL, true);

	// requestHeader 설정 : Content-Type 지정
	xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

	// 작업이 완료된 후 호출할 callback 메소드 지정
	xhr.onreadystatechange = callback;

	// Ajax 요청을 서버로 전달
	xhr.send(httpMethod == 'POST' ? httpParam : null);
}

DTO와 DAO

  1. dto
package dto;

import lombok.Data;

@Data
public class BoardDTO {
	private int idx;
	private int readhit;
	private int ref;
	private int step;
	private int depth;
	private int del_info;
	
	private String name;
	private String subject;
	private String content;
	private String pwd;
	private String ip;
	private String regdate;
}
  1. dao
package dao;

import java.util.HashMap;
import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import dto.BoardDTO;
import service.MybatisConnector;

public class BoardDAO {

	static BoardDAO single = null;
	SqlSessionFactory factory;
	
	public static BoardDAO getInstance() {
		if (single == null) {
			single = new BoardDAO();
		}
		return single;
	}
	
	public BoardDAO() {
		factory = MybatisConnector.getInstance().getsqlSessionFactory();
	}
	
	// 게시글 조회하기
	public List<BoardDTO> select(HashMap<String, Integer> map) {
		SqlSession sqlSession = factory.openSession();
		List<BoardDTO> list = sqlSession.selectList("b.board_list", map);
		sqlSession.close();
		return list;
	}
	
	// 전체 게시글 조회하기
	public int getRowTotal() {
		SqlSession sqlSession = factory.openSession();
		int count = sqlSession.selectOne("b.board_count");
		sqlSession.close();
		return count;
	}
	
	// 게시글 추가하기
	public int insert(BoardDTO dto) {
		// DB에 commit을 위해 openSession(true)
		SqlSession sqlSession = factory.openSession(true);
		int res = sqlSession.insert("b.board_insert", dto);
		sqlSession.close();
		return res;
	}

	// 게시글 상세보기
	public BoardDTO selectOne(int idx) {
		SqlSession sqlSession = factory.openSession();
		BoardDTO dto = sqlSession.selectOne("b.board_one", idx);
		sqlSession.close();
		return dto;
	}

	// 조회수 증가하기
	public int update_readhit(int idx) {
		SqlSession sqlSession = factory.openSession(true);
		int res = sqlSession.update("b.board_update_readhit", idx);
		sqlSession.close();
		return res;
	}
	
	// 답글 추가를 위한 step = step + 1
	public int update_step(BoardDTO dto) {
		SqlSession sqlSession = factory.openSession(true);
		int res = sqlSession.update("b.board_update_step", dto);
		sqlSession.close();
		return res;
	}

	// 답글 등록
	public int reply(BoardDTO dto) {
		SqlSession sqlSession = factory.openSession(true);
		int res = sqlSession.insert("b.board_reply", dto);
		sqlSession.close();
		return res;
	}
	
	// 삭제된 것처럼 수정
	public int del_update(BoardDTO dto) {
		SqlSession sqlSession = factory.openSession(true);
		int res = sqlSession.update("b.board_del", dto);
		sqlSession.close();
		return res;
	}
}

mapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="b">
	<!-- 전체 게시글 조회 -->
	<select id="board_list" parameterType="java.util.HashMap" resultType="board">
		SELECT * FROM (SELECT RANK() OVER(ORDER BY ref DESC, step) AS no, b.* FROM BOARD b)
		WHERE no BETWEEN #{start} AND #{end}
	</select>
	
	<!-- 전체 게시글 조회 -->
	<select id="board_count" resultType="int">
		SELECT COUNT(*) FROM BOARD
	</select>
	
	<!-- 게시글 추가 -->
	<insert id="board_insert" parameterType="board">
		INSERT INTO BOARD VALUES(
			SEQ_BOARD_IDX.nextVal,
			#{name},
			#{subject},
			#{content},
			#{pwd},
			#{ip},
			SYSDATE,
			0,
			SEQ_BOARD_IDX.currVal,
			0,
			0,
			0
		)
	</insert>
	
	<!-- 게시글 상세보기 -->
	<select id="board_one" parameterType="int" resultType="board">
		SELECT * FROM BOARD WHERE IDX = #{idx}
	</select>
	
	<!-- 조회수 증가 -->
	<update id="board_update_readhit" parameterType="int">
		UPDATE BOARD
		SET READHIT = READHIT + 1
		WHERE IDX = #{idx}
	</update>
	
	<!-- 답글 작성을 위한 step 증가 -->
	<!-- 현재 글의 step보다 step이 큰 글들의 step 증가 -->
	<update id="board_update_step" parameterType="board">
		UPDATE BOARD
		SET STEP = STEP + 1
		WHERE REF = #{ref} AND STEP > #{step}
	</update>
	
	<!-- 답글 추가 -->
	<insert id="board_reply" parameterType="board">
		INSERT INTO BOARD VALUES(
			SEQ_BOARD_IDX.nextVal,
			#{name},
			#{subject},
			#{content},
			#{pwd},
			#{ip},
			SYSDATE,
			0,
			#{ref},
			#{step},
			#{depth},
			0
		)
	</insert>
	
	<!-- 게시글이 삭제된 것처럼 처리 -->
	<update id="board_del" parameterType="board">
		UPDATE BOARD
		SET SUBJECT = #{subject},
			NAME = #{name},
			DEL_INFO = -1
		WHERE IDX = #{idx}
	</update>
</mapper>

JSP

  1. 게시판 메인 화면 jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<style type="text/css">
		a{text-decoration:none;}
		table{width:700px; 
			border-collapse:collapse;}
		table h1{text-align:center;}
	</style>
</head>
<body>
	<table border="1" align="center">
		<tr>
			<td colspan="5"><h1>게시판</h1></td>
		</tr>
		<tr>
			<th>번호</th>
			<th>제목</th>
			<th>작성자</th>
			<th>작성일</th>
			<th>조회수</th>
		</tr>
			
		<c:forEach var="dto" items="${list}">
			<tr>
				<td align="center">${dto.idx}</td>
				
				<td>
					<!-- 댓글 indent -->
					<c:forEach begin="1" end="${dto.depth}">&nbsp;</c:forEach>
					<c:if test="${dto.depth ne 0}">ㄴ</c:if>
					
					<!-- 삭제되지 않은 글 일때만 클릭 가능 -->
					<c:if test="${dto.del_info ne -1}">
						<a href="view?idx=${dto.idx}&page=${param.page}">
							<font color="black">${dto.subject}</font>
						</a>
					</c:if>
					
					<!-- 삭제된 글은 클릭 불가 -->
					<c:if test="${dto.del_info eq -1}">
						<font color="gray">${dto.subject}</font>
					</c:if>
				</td>
				
				<td>${dto.name}</td>
				<td>${fn:split(dto.regdate,' ')[0]}</td>
				<td>${dto.readhit}</td>
			</tr>
		</c:forEach>
			<tr>
				<!-- 페이지 넘어가는 하단 메뉴바 -->
				<td colspan="5" align="center"> ${pageMenu} </td>
			</tr>
			<tr>
				<td colspan="5" align="right">
					<input type="button" value="글작성" onclick="location.href='insert_form.jsp'">
				</td>
			</tr>
	</table>
</body>
</html>
  1. 게시글 추가하기 jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script type="text/javascript">
		function send() {
			var f = document.f;
			var subject = f.subject.value;
			var name = f.name.value;
			var content = f.content.value;
			var pwd = f.pwd.value;
			
			// 유효성 검사
			if (subject == '') {
				alert('제목을 입력하세요');
				return;
			}
			if (name == '') {
				alert('이름을 입력하세요');
				return;
			}
			if (content == '') {
				alert('내용을 한 글자 이상 입력하세요');
				return;
			}
			if (pwd == '') {
				alert('비밀번호를 입력하세요');
				return;
			}
			
			f.submit();
		}
	</script>
</head>
<body>
	<form name="f" method="POST" action="insert">
		<table border="1" align="center">
			<caption>::새 글 쓰기::</caption>
			<tr>
				<th>제목</th>
				<td><input name="subject"></td>
			</tr>
			<tr>
				<th>작성자</th>
				<td><input name="name"></td>
			</tr>
			<tr>
				<th>내용</th>
				<td>
					<textarea name="content" rows="10" cols="50" style="resize:none;"></textarea>
				</td>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<td colspan="2">
					<input type="button" value="등록하기" onclick="send()">
					<input type="button" value="목록으로 돌아가기" onclick="location.href='board_list'">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. 답글 달기 jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script type="text/javascript">
		function send() {
			var f = document.f;
			var subject = f.subject.value;
			var name = f.name.value;
			var content = f.content.value;
			var pwd = f.pwd.value;
			
			// 유효성 검사
			if (subject == '') {
				alert('제목을 입력하세요');
				return;
			}
			if (name == '') {
				alert('이름을 입력하세요');
				return;
			}
			if (content == '') {
				alert('내용을 한 글자 이상 입력하세요');
				return;
			}
			if (pwd == '') {
				alert('비밀번호를 입력하세요');
				return;
			}
			
			f.submit();
		}
	</script>
</head>
<body>
	<form name="f" method="POST" action="reply">
		<input type="hidden" name="idx" value="${param.idx}">
		<input type="hidden" name="page" value="${param.page}">
		
		<table border="1" align="center">
			<caption>::댓글 쓰기::</caption>
			<tr>
				<th>제목</th>
				<td><input name="subject"></td>
			</tr>
			<tr>
				<th>작성자</th>
				<td><input name="name"></td>
			</tr>
			<tr>
				<th>내용</th>
				<td>
					<textarea name="content" rows="10" cols="50" style="resize:none;"></textarea>
				</td>
			<tr>
				<th>비밀번호</th>
				<td><input name="pwd" type="password"></td>
			</tr>
			<tr>
				<td colspan="2">
					<input type="button" value="등록하기" onclick="send()">
					<input type="button" value="목록으로 돌아가기" onclick="location.href='board_list?page=${param.page}'">
				</td>
			</tr>
		</table>
	</form>
</body>
</html>
  1. 게시글 상세보기 jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
	<script src="js/HttpRequest.js"></script>
	<script type="text/javascript">
		function reply() {
			location.href = "reply_form.jsp?idx=${dto.idx}&page=${param.page}";
		}
		
		function del() {
			if (!confirm('삭제하시겠습니까?')) {
				return;
			}
			
			var pwd = "${dto.pwd}";
			var c_pwd = document.getElementById("c_pwd").value;
			if (c_pwd != pwd) {
				alert('비밀번호가 일치하지 않습니다');
				return;
			}
			
			var url = "del";
			var param = "idx=${dto.idx}";
			
			sendRequest(url, param, delCheck, "POST");
		}
		
		function delCheck() {
			if(xhr.readyState == 4 && xhr.status == 200) {
				var data = xhr.responseText;
				var json = eval(data);
				
				if(json[0].param == "yes") {
					alert('게시글이 정상적으로 삭제되었습니다');
					location.href = "board_list?page=${param.page}";
				} else {
					alert('게시글 삭제에 실패했습니다');
				}
			}
		}
	</script>
</head>
<body>
	<table border="1" align="center">
		<caption>:::게시글 상세보기:::</caption>
		<tr>
			<th>제목</th>
			<td>${dto.subject}</td>
		</tr>
		<tr>
			<th>작성자</th>
			<td>${dto.name}</td>
		</tr>
		<tr>
			<th>작성일</th>
			<td>${dto.regdate}</td>
		</tr>
		<tr>
			<th>내용</th>
			<td width="500px" height="200px">
				<pre>${dto.content}</pre>
			</td>
		</tr>
		<tr>
			<th>조회수</th>
			<td>${dto.readhit}</td>
		</tr>
		<tr>
			<th>비밀번호</th>
			<td><input type="password" id="c_pwd"></td>
		</tr>	
		<tr>
			<td colspan="2">
				<!-- 목록으로 돌아가기 -->
				<input type="button" value="목록으로 돌아가기" onclick="location.href='board_list?page=${param.page}'">
				
				<!-- 댓글의 댓글을 무한 생성 못하게 설정 -->
				<c:if test="${dto.depth lt 1}">
				<!-- 댓글 추가 -->
					<input type="button" value="댓글 달기" onclick="reply();">
				</c:if>
				<!-- 글삭제 -->
				<input type="button" value="글삭제" onclick="del();">
			</td>
		</tr>
	</table>
</body>
</html>

Servlet

  1. 게시판 조회 Servlet
package action;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.BoardDAO;
import dto.BoardDTO;
import util.Common;
import util.Paging;

@WebServlet("/board_list")
public class BoardListAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// 글자가 안 깨지도록 설정
		request.setCharacterEncoding("utf-8");
		
		int nowPage = 1;
		String page = request.getParameter("page");
		if (page != null && !page.isEmpty()) { 
			// board_list --> null
			// board_list?page= --> empty
			nowPage = Integer.parseInt(page);
		}
		
		// 한 페이지에 표시될 게시물의 시작과 끝 번호 계산
		// page = 1 --> 1 ~ 10
		// page = 2 --> 11 ~ 20
		int start = (nowPage - 1) * Common.Board.BLOCKLIST + 1;
		int end = start + Common.Board.BLOCKLIST - 1;
		
		HashMap<String, Integer> map = new HashMap<String, Integer>();
		map.put("start", start);
		map.put("end", end);
		
		// dao객체 만들기 -- DB에 여러 번 접근하기 위해
		BoardDAO dao = BoardDAO.getInstance();
		
		// 게시글 전체 목록 조회
		List<BoardDTO> list = dao.select(map);
		
		// 게시글 전체 수 가져오기
		int rowTotal = dao.getRowTotal();
		
		// 페이지 메뉴 생성
		String pageMenu = Paging.getPaging("board_list", nowPage, rowTotal, Common.Board.BLOCKLIST, Common.Board.BLOCKPAGE);
		
		// 새로 페이지 접근 시 조회수 증가를 위해 session의 show 제거
		// session은 /board_view에서 생성됨
		request.getSession().removeAttribute("show");
		
		// 바인딩
		request.setAttribute("list", list);
		request.setAttribute("pageMenu", pageMenu);
		
		// 포워딩
		RequestDispatcher disp = request.getRequestDispatcher("board_list.jsp?page="+nowPage);
		disp.forward(request, response);
	}

}
  1. 게시판 추가 Servlet
package action;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.BoardDAO;
import dto.BoardDTO;

@WebServlet("/insert")
public class BoardInsertAction extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
   @Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
	   // 글이 깨지지 않게 인코딩 설정
	   request.setCharacterEncoding("utf-8");
	   
	   String subject = request.getParameter("subject");
	   String name = request.getParameter("name");
	   String content = request.getParameter("content");
	   String pwd = request.getParameter("pwd");
	   String ip = request.getRemoteAddr();
	   
	   BoardDTO dto = new BoardDTO();
	   dto.setSubject(subject);
	   dto.setName(name);
	   dto.setContent(content);
	   dto.setPwd(pwd);
	   dto.setIp(ip);
	   
	   int res = BoardDAO.getInstance().insert(dto);
	   
	   if (res > 0) {
		   response.sendRedirect("board_list");
	   }
	}
}
  1. 게시판 답글 Servlet
package action;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.BoardDAO;
import dto.BoardDTO;

@WebServlet("/reply")
public class BoardReplyAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		// 글이 깨지지 않게 인코딩 설정
		request.setCharacterEncoding("utf-8");

		// 페이지 설정 -- 답글 등록 후 같은 페이지에 남기
		int nowPage = 1;
		String page = request.getParameter("page");
		if (page != null && !page.isEmpty()) {
			nowPage = Integer.parseInt(page);
		}
		
		int idx = Integer.parseInt(request.getParameter("idx"));
		String subject = request.getParameter("subject");
		String name = request.getParameter("name");
		String content = request.getParameter("content");
		String pwd = request.getParameter("pwd");
		String ip = request.getRemoteAddr();

		// dao객체 생성해두기 -- DB에 2번 이상 접근하기 위해
		BoardDAO dao =	BoardDAO.getInstance();
		
		// 같은 ref를 가진 데이터들 중 내가 추가하려는 step 값 이상인 글들을
		// (step + 1) 처리해야 함 ==> 게시글이 최신 작성 순으로 정렬됨
		// 기준글의 idx를 이용해서 댓글을 달고 싶은 게시글의 정보 가져오기
		BoardDTO base_dto = dao.selectOne(idx);
		
		// 현재 댓글의 step 이상인 댓글들을 모두 (step + 1) 처리
		// 결과 반환은 다른 곳에서 호출 안해둠 --> 오류 발생 시 사용해볼 것
		int res_step = dao.update_step(base_dto);
		
		// 댓글 정보 저장 및 댓글 등록
		BoardDTO dto = new BoardDTO();
		dto.setSubject(subject);
		dto.setName(name);
		dto.setContent(content);
		dto.setPwd(pwd);
		dto.setIp(ip);
		dto.setRef(base_dto.getRef());
		dto.setStep(base_dto.getStep()+1);
		dto.setDepth(base_dto.getDepth()+1);
		
		int res = dao.reply(dto);

		if (res > 0) {
			response.sendRedirect("board_list?page="+nowPage);
		}
	}

}
  1. 게시판 상세보기 Servlet
package action;

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import dao.BoardDAO;
import dto.BoardDTO;

@WebServlet("/view")
public class BoardViewAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// 글이 깨지지 않게 인코딩 설정
		request.setCharacterEncoding("utf-8");
		
		// 페이지 설정 -- 조회수를 의도적으로 올리는 시도 방지
		int nowPage = 1;
		String page = request.getParameter("page");
		if (page != null && !page.isEmpty()) {
			nowPage = Integer.parseInt(page);
		}
		
		int idx = Integer.parseInt(request.getParameter("idx"));
		
		// dao 객체 생성 -- DB에 여러 번 접근
		BoardDAO dao = BoardDAO.getInstance();
		
		// idx에 해당하는 게시글 조회
		BoardDTO dto = dao.selectOne(idx);
		
		// Session을 이용해서 새로고침 시 조회수 증가 방지
		HttpSession session = request.getSession();
		
		String show = (String)session.getAttribute("show");
		if (show == null) { // session 최초 1회는 값이 없므으로 조회수 증가
			// 조회수 증가
			int res = dao.update_readhit(idx);
			
			// show에 아무 값을 넣어서 session이 값을 가지도록 설정
			// 값을 가진 동안에는 조회수 증가 방지
			session.setAttribute("show", "0");
		}
		
		// 바인딩
		request.setAttribute("dto", dto);
		
		// 포워딩
		RequestDispatcher disp = request.getRequestDispatcher("board_view.jsp?page="+nowPage);
		disp.forward(request, response);
	}

}
  1. 게시판 삭제 Servlet
package action;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import dao.BoardDAO;
import dto.BoardDTO;

@WebServlet("/del")
public class BoardDeleteAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// del?idx=${dto.idx}
		int idx = Integer.parseInt(request.getParameter("idx"));
		
		// dao 객체 만들기 -- DB에 여러 번 접근을 위해
		BoardDAO dao = BoardDAO.getInstance();
		
		BoardDTO baseDTO = dao.selectOne(idx);
		
		baseDTO.setSubject("삭제된 글 입니다");
		baseDTO.setName("unknown");
		
		int res = dao.del_update(baseDTO);
		
		if (res > 0) {
			response.getWriter().print("[{'param':'yes'}]");
		} else {
			response.getWriter().print("[{'param':'no'}]");
		}
	}

}

util 클래스

package util;

public class Common {
	// 게시판용 정적 내부 클래스
	// 외부 클래스에서 객체화 없이 바로 사용할 수 있다.
	// 다만 내부 정적 클래스에서 외부 클래스의 필드 호출 시 static이 아니라면 사용할 수 없다.
	public static class Board{
		// 한 페이지에 보여줄 게시물 개수
		public final static int BLOCKLIST = 10;
		
		// 보여줄 페이지 메뉴의 수
		public final static int BLOCKPAGE = 3;
	}
}
package util;

public class Paging {
	
	// 페이징 처리 메소드
	public static String getPaging(String pageURL, int nowPage, int rowTotal, int blockList, int blockPage) {
		
		int totalPage; // 전체 페이지 수
		int startPage; // 시작 페이지 번호
		int endPage; // 마지막 페이지 번호
		boolean isPrevPage, isNextPage;
		
		StringBuffer sb; // HTML 코드 작성
		
		isPrevPage = isNextPage = false;
		
		// 입력된 전제 자원을 통해 전체 페이지 수 계산
		// 총 게시글 수 / 한 페이지에 보여줄 게시글 수
		totalPage = (int)(rowTotal/blockList);
		if (rowTotal % blockList != 0) totalPage++;
		
		if (nowPage > totalPage) { // totalPage 초과해서 넘어가지 않게 설정
			nowPage = totalPage;
		}
		
		// 시작 페이지와 마지막 페이지
		// 게시판 하단에 보여줄 목록 숫자 --> 1 2 3 --> 4 5 6
		startPage = (int)(((nowPage - 1) / blockPage) * blockPage + 1);
		endPage = startPage + blockPage - 1;
		
		// 마지막 페이지가 전체 페이지보다 작을 때, 시작 페이지가 1보다 클 때 이동버튼 작동 활성화
		if (endPage < totalPage) isNextPage = true;
		if (startPage > 1) isPrevPage = true;
		
		// HTML 작성
		sb = new StringBuffer();
		
		if(isPrevPage) { // startPage >= 2
			sb.append("<a href='"+pageURL+"?page="+(startPage-1)+"'>");
			sb.append("◀");
			sb.append("</a>");
		} else {
			sb.append("<span>◀</span>");
		}
		
		sb.append("");
		
		for (int i = startPage; i <= endPage; i++) {
			if (i > totalPage) break;
			if (i == nowPage) { // 현재 페이지 강조
				sb.append("&nbsp;<b><font color='#ff0000'>");
				sb.append(i);
				sb.append("</font></b>");
			} else {
				sb.append("&nbsp;<a href='"+pageURL+"?page="+i+"'>");
				sb.append(i);
				sb.append("</a>");
			}
		}
		
		sb.append("&nbsp; ");
		
		if(isNextPage) { // endPage < totalPage
			sb.append("<a href='"+pageURL+"?page="+(endPage+1)+"'>");
			sb.append("▶");
			sb.append("</a>");
		} else {
			sb.append("<span>▶</span>");
		}
		
		return sb.toString();
	}
}

완성된 모습

  1. ListAction Servlet에서 실행하면 게시판 사이트가 로딩된다. 미리 추가해둔 샘플 데이터들을 확인할 수 있다.
    게시판 1.png

  2. 게시글을 누르면 상세하게 볼 수 있다.
    게시판 2.png

  3. 댓글을 작성하고 등록하면 새로 추가된 댓글을 볼 수 있다. 그리고 첫 게시글을 한 번 접속했기 때문에 조회수도 1 증가했다.

  4. 댓글의 배치를 잘 보면 4번 글이 나중에 추가된 댓글인데 해당 댓글이 가장 위에 있다.
    게시판 3.png
    게시판 4.png

  5. 게시글을 작성할 때 제목, 이름, 내용, 비밀번호를 입력해야만 등록 가능하도록 설정한 것을 직접 확인할 수 있다.
    게시판 11-1.png
    게시판 11-2.png
    게시판 11-3.png
    게시판 11-4.png

  6. 필요한 내용을 모두 작성했다면 게시글이 성공적으로 등록된다. 게시글 중에선 가장 최근 추가된 글이기 때문에 번호가 5번이지만 가장 위에 있다.
    게시판 5.png
    게시판 6.png

  7. 댓글을 들어가 삭제 버튼을 누르면 비밀번호 일치를 확인한다. 일치하는 경우에만 정상적으로 삭제된다는 메시지가 뜬다.
    게시판 7.png
    게시판 8.png
    게시판 9.png

  8. 삭제된 글은 제목이 "삭제된 글 입니다"로 바뀌고, 작성자도 "unknown"으로 바뀌며, 클릭할 수 없다.
    게시판 10.png

  9. 조금 시간을 들여 이번엔 페이지 넘어가는 기능이 잘 들어갔는지 확인하기 위해 글을 30개 정도 추가 작성한다.

  10. 한 페이지에 게시글이 총 10개 보이며, 하단 메뉴바에서 다음 페이지로 넘어가는 버튼과 숫자들이 제대로 추가되었다.
    게시판 12.png

  11. 해당 페이지 번호를 누르면 페이지가 정상적으로 로딩 된다.
    게시판 13.png

  12. 다음 페이지 목록을 보기 위해 넘어가는 버튼을 누르면 4 페이지가 뜬다.
    게시판 14.png

  13. 3 페이지에서 아무 글이나 선택해서 댓글을 작성해본다.
    게시판 15-1.png
    게시판 15-2.png

  14. 댓글 작성 후에도 3페이지에 남아 있는 것을 확인할 수 있다.
    게시판 15-3.png